#!/usr/bin/env bash

# For the license, see the LICENSE file in the root directory.

ROOT=${abs_top_builddir:-$(dirname "$0")/..}
TESTDIR=${abs_top_testdir:-$(dirname "$0")}

source "${TESTDIR}/common"
skip_test_no_tpm20 "${SWTPM_EXE}"

workdir="$(mktemp -d)" || exit 1
export TPM_PATH=${workdir}
SWTPM_SERVER_PORT=65480
SWTPM_CTRL_PORT=65481
SWTPM_SERVER_NAME=127.0.0.1
SWTPM_INTERFACE="socket+socket"

trap "cleanup" SIGTERM EXIT


cat <<_EOF_ > "${workdir}/swtpm-localca.options"
--tpm-manufacturer IBM
--tpm-model swtpm-libtpms
--tpm-version 2
--platform-manufacturer "Fedora XYZ"
--platform-version 2.1
--platform-model "QEMU A.B"
_EOF_

cat <<_EOF_ > "${workdir}/swtpm_setup.conf"
create_certs_tool=\${SWTPM_LOCALCA}
create_certs_tool_config=${workdir}/swtpm-localca.conf
create_certs_tool_options=${workdir}/swtpm-localca.options
_EOF_

function cleanup()
{
	rm -rf "${workdir}"

	if kill_quiet -0 "${SWTPM_PID}"; then
		kill_quiet -9 "${SWTPM_PID}"
	fi
}

PROFILE_V2=""
if ${SWTPM_SETUP} \
	--tpm2 \
	--tpm "${SWTPM_EXE} socket ${SWTPM_TEST_SECCOMP_OPT}" \
	--print-capabilities \
	| grep -q '"default-v2"' ; then
	PROFILE_V2="default-v2"
fi


test_swtpm_setup_profile()
{
	local workdir="${1}"
	local profile="${2}"
	local exp_response="${3}"	# expected 'ActiveProfile' response; optional
	local disabled_algos="${4}"	# disabled algorithms; optional
	local tpm2request="${5}"	# TPM 2 request to send; optional
	local exp_tpm2response="${6}"	# expected response; optional
	local exp_fail="${7}"		# swtpm_setup is expexted to fail when '1'

	local response

	rm -f "${workdir}/logfile"

	if ! $SWTPM_SETUP \
		--tpm2 \
		--tpmstate "${workdir}" \
		--config "${workdir}/swtpm_setup.conf" \
		--log "${workdir}/logfile" \
		--tpm "${SWTPM_EXE} socket ${SWTPM_TEST_SECCOMP_OPT}" \
		--overwrite \
		${profile:+--profile ${profile}}
	then
		if [ "${exp_fail}" -eq 1 ]; then
			# swtpm_setup failed as expected
			return 0
		fi
		echo "Test failed: Error: Could not run $SWTPM_SETUP."
		echo "Setup Logfile:"
		cat "${workdir}/logfile"
		exit 1
	fi
	if [ -n "${exp_fail}" ] && [ "${exp_fail}" -eq 1 ]; then
		echo "Error: swtpm_setup did not fail as expected"
		exit 1
	fi

	run_swtpm "${SWTPM_INTERFACE}" \
		--tpm2 \
		--flags not-need-init,startup-clear

	if ! kill_quiet -0 "${SWTPM_PID}"; then
		echo "Error: ${SWTPM_INTERFACE} TPM did not start."
		exit 1
	fi

	if [ -n "${exp_response}" ]; then
		response=$(run_swtpm_ioctl "${SWTPM_INTERFACE}" --info 0x20)
		if ! [[ "${response}" =~ ${exp_response} ]]; then
			echo "Error: Response does not match expected response regular expression"
			echo "Actual   : ${response}"
			echo "Expected : ${exp_response}"
			exit 1
		fi
	fi

	if [ -n "${disabled_algos}" ]; then
		response=$(run_swtpm_ioctl "${SWTPM_INTERFACE}" --info 0x8 |
			   sed -n 's/.*"Disabled":"\([^"]*\).*/\1/p')
		if [ "${response}" != "${disabled_algos}" ]; then
			echo "Error: Response for disabled algorithms does not match expected response"
			echo "Actual   : ${response}"
			echo "Expected : ${disabled_algos}"
			exit 1
		fi
	fi

	if [ -n "${tpm2request}" ]; then
		response=$(swtpm_cmd_tx "${SWTPM_INTERFACE}" "${tpm2request}")
		if [ "${response}" != "${exp_tpm2response}" ]; then
			echo "Error: TPM 2 response does not match expected response"
			echo "Actual   : ${response}"
			echo "Expected : ${exp_tpm2response}"
			exit 1
		fi
	fi

	if ! run_swtpm_ioctl "${SWTPM_INTERFACE}" -s; then
		echo "Error: Could not shut down the ${SWTPM_INTERFACE} TPM."
		exit 1
	fi

	if wait_process_gone "${SWTPM_PID}" 4; then
		echo "Error: ${SWTPM_INTERFACE} TPM should not be running anymore."
		exit 1
	fi

	# Expecting same results from swtpm directly
	if [ -n "${exp_response}" ]; then
		response=$(${SWTPM_EXE} socket \
				--tpmstate "dir=${workdir}"  \
				--tpm2 \
				--print-info 0x20)
		if ! [[ "${response}" =~ ${exp_response} ]]; then
			echo "Error: Response does not match expected response regular expression (swtpm)"
			echo "Actual   : ${response}"
			echo "Expected : ${exp_response}"
			exit 1
		fi
	fi
}

# Get available profiles and algorithms that are implemented and those that can be disabled
run_swtpm "${SWTPM_INTERFACE}" \
	--tpm2 \
	--flags not-need-init

if ! kill_quiet -0 "${SWTPM_PID}"; then
	echo "Error: ${SWTPM_INTERFACE} TPM did not start."
	exit 1
fi

profiles=$(run_swtpm_ioctl "${SWTPM_INTERFACE}" --info 0x40 |
	   sed $'s/{"Name/\\\n{"Name/g' |
	   sed -n 's/^{"Name":"\([^"]*\)".*/\1/p')

canbedisabled_algos_str=$(run_swtpm_ioctl "${SWTPM_INTERFACE}" --info 0x8 |
			  sed -n 's/.*"CanBeDisabled":"\([^"]*\).*/\1/p')

implemented_algos_str=$(run_swtpm_ioctl "${SWTPM_INTERFACE}" --info 0x8 |
			sed -n 's/.*"Implemented":"\([^"]*\).*/\1/p')

if ! run_swtpm_ioctl "${SWTPM_INTERFACE}" -s; then
	echo "Error: Could not shut down the ${SWTPM_INTERFACE} TPM."
	exit 1
fi

if wait_process_gone "${SWTPM_PID}" 4; then
	echo "Error: ${SWTPM_INTERFACE} TPM should not be running anymore."
	exit 1
fi

# Test with no profile
exp_response=$(echo "^\{\"ActiveProfile\":\{" \
               "\"Name\":\"null\"," \
               "\"StateFormatLevel\":[0-9]+,"\
               "\"Commands\":\"[,x[:xdigit:]-]+\","\
               "\"Algorithms\":\"[,=[:alnum:]-]+\","\
               "\"Description\":\"[[:print:]]+\""\
               "\}\}\$"| tr -d " ")
test_swtpm_setup_profile \
	"${workdir}" "" "${exp_response}" \
	"" "" "" ""

echo "Test without profile passed"

# Test-setup each profile that is implemented
for profile in ${profiles}; do
	exp_response=$(echo "^\{\"ActiveProfile\":\{" \
	               "\"Name\":\"${profile}\"," \
	               "\"StateFormatLevel\":[0-9]+,"\
	               "\"Commands\":\"[,x[:xdigit:]-]+\","\
	               "\"Algorithms\":\"[,=[:alnum:]-]+\","\
	               "(\"Attributes\":\"[,=[:alnum:]-]+\",)?"\
	               "\"Description\":\"[[:print:]]+\""\
	               "\}\}\$"| tr -d " ")
	test_swtpm_setup_profile \
		"${workdir}" "{\"Name\":\"${profile}\"}" "${exp_response}" "" "" "" ""

	echo "Test with profile '${profile}' passed"
done

# Setup the custom profile with an increasing set of disabled algorithms
# that swtpm_ioctl has to show then.
IFS="," read -r -a canbedisabled_algos <<< "${canbedisabled_algos_str}"
IFS="," read -r -a implemented_algos <<< "${implemented_algos_str}"

algos="${implemented_algos[*]}"
disabled=()
for todisable in "${canbedisabled_algos[@]}"; do
	# Remove algorithm ${todisable} from list of implemented algorithms
	algos=$(echo "${algos}" | sed "s/${todisable}//" | tr -s ' ')
	disabled+=("${todisable}")

	# Format profile
	a=$(echo "${algos}" | sed -e 's/ /,/g' -e 's/,$//')
	profile="{\"Name\":\"custom\",\"Algorithms\":\"${a}\"}"

        d=${disabled[*]}
        d=${d// /,}

	test_swtpm_setup_profile \
		"${workdir}" "${profile}" "" "${d}" "" "" ""
	echo "Test with custom profile and disabled algorithms '${d}' passed"
done

# Test the custom profiles with disabled commands and various StateFormatLevels
test_custom_profile_sfls()
{
	local cmds="$1"
	local sfls_list="$2"
	local msg="$3"

	local sfl exp_sfl exp_fail profile exp_response

	for sfls in ${sfls_list}; do
		sfl=$(cut -d":" -f1 <<< "${sfls}")
		exp_sfl=$(cut -d":" -f2 <<< "${sfls}")
		exp_fail=$(cut -d":" -f3 <<< "${sfls}")
		profile="{\"Name\":\"custom:test-sfl-${sfl}\",\"Commands\":\"${cmds}\",\"StateFormatLevel\":${sfl}}"
		exp_response=".*,\"StateFormatLevel\":${exp_sfl},.*"
		test_swtpm_setup_profile \
			"${workdir}" "${profile}" "${exp_response}" "" "" "" "${exp_fail}"
		echo "Test with custom profile and commands${msg} passed with StateFormatLevel ${sfl}."
	done
}

# because of 0x199-0x19a we must have StateFormatLevel >=3 for it to be acceptable
cmds="0x11f-0x122,0x124-0x12e,0x130-0x140,0x142-0x159,0x15b-0x15e,0x160-0x165,0x167-0x174,0x176-0x178,0x17a-0x193,0x199-0x19a"
test_custom_profile_sfls "${cmds}" "1:f:1 2:f:1 3:3:0 4:4:0 5:5:0 6:6:0 7:7:0" " (with 0x199-0x19a)"

# because of 0x19b we must have StateFormatLevel >=5 for it to be acceptable
cmds="0x11f-0x122,0x124-0x12e,0x130-0x140,0x142-0x159,0x15b-0x15e,0x160-0x165,0x167-0x174,0x176-0x178,0x17a-0x193,0x199-0x19a,0x19b"
test_custom_profile_sfls "${cmds}" "1:f:1 2:f:1 3:f:1 4:f:1 5:5:0 6:6:0 7:7:0" " (with 0x199-0x19b)"

# Without given StateFormatLevel it should go up to StateFormatLevel '4' (due to AES-192 being selected)
profile="{\"Name\":\"custom\",\"Commands\":\"${cmds}\"}"
exp_response=".*,\"StateFormatLevel\":[[:digit:]]+,.*"
test_swtpm_setup_profile \
	"${workdir}" "${profile}" "${exp_response}" "" "" "" "0"

# with cmds 0x199-0x19a missing it can have many StateFormatLevels >= 2
cmds="0x11f-0x122,0x124-0x12e,0x130-0x140,0x142-0x159,0x15b-0x15e,0x160-0x165,0x167-0x174,0x176-0x178,0x17a-0x193"
# StateFormatLevel 3 enables 'nothing' additional but will be kept
# StateFormatLevel 4 enables AES-192 and will remain at '4'
test_custom_profile_sfls "${cmds}" "1:f:1 2:2:0 3:3:0 4:4:0 5:5:0 6:6:0 7:7:0" ""

# Without given StateFormatLevel it should go up to StateFormatLevel '4' (due to AES-192 being selected)
profile="{\"Name\":\"custom\",\"Commands\":\"${cmds}\"}"
exp_response=".*,\"StateFormatLevel\":[[:digit:]]+,.*"
test_swtpm_setup_profile \
	"${workdir}" "${profile}" "${exp_response}" "" "" "" "${exp_fail}"

# Setup the TPM 2 with custom profile and sha1 removed
# Check that the GetCapability command returns the proper set of PCR banks

algos="${implemented_algos[*]}"
algos=$(echo "${algos}" | sed "s/sha1//" | tr -s ' ')
# Format profile
a=$(echo "${algos}" | sed -e 's/ /,/g' -e 's/,$//')
profile="{\"Name\":\"custom:no-sha1\",\"Algorithms\":\"${a}\"}"

# Check PCR bank sha1 is not available: tssgetcapability -cap 5
test_swtpm_setup_profile \
	"${workdir}" \
	"${profile}" \
	"" \
	"" \
	"\x80\x01\x00\x00\x00\x16\x00\x00\x01\x7a\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x40" \
	" 80 01 00 00 00 25 00 00 00 00 00 00 00 00 05 00 00 00 03 00 0b 03 ff ff ff 00 0c 03 00 00 00 00 0d 03 00 00 00" \
	""

echo "Test with custom profile and 'sha1' disabled algorithms passed"

# Check that swtpm adjusts key sizes and sets 'Attributes' when
# '--profile-remove-disabled fips-host' is passed
profile="{\"Name\":\"custom\",\"Description\":\"Test\"} --profile-remove-disabled fips-host"
exp_response="\{\"ActiveProfile\":\{\"Name\":\"custom\",.*,\"Algorithms\":\".*,rsa-min-size=2048,.*,ecc-min-size=224,.*\",\"Attributes\":\"no-sha1-signing,no-sha1-verification,no-unpadded-encryption\",\"Description\":\"Test\"\}\}"
test_swtpm_setup_profile "${workdir}" "${profile}" "${exp_response}" "" "" "" "0"

profile="{\"Name\":\"custom\",\"Attributes\":\"no-sha1-signing\",\"Description\":\"Test\"} --profile-remove-disabled fips-host"
test_swtpm_setup_profile "${workdir}" "${profile}" "${exp_response}" "" "" "" "0"

profile="{\"Name\":\"custom:test\",\"Attributes\":\"fips-host\",\"Description\":\"Test\"} --profile-remove-disabled fips-host"
exp_response="\{\"ActiveProfile\":\{\"Name\":\"custom:test\",.*,\"Algorithms\":\".*,rsa-min-size=2048,.*,ecc-min-size=224,.*\",\"Attributes\":\"fips-host\",\"Description\":\"Test\"\}\}"
test_swtpm_setup_profile "${workdir}" "${profile}" "${exp_response}" "" "" "" "0"

# --profile-remove-disabled must not have any effect on other profiles
for p in null default-v1 ${PROFILE_V2}; do
	profile="{\"Name\":\"${p}\"} --profile-remove-disabled fips-host"
	exp_response="\{\"ActiveProfile\":\{\"Name\":\"${p}\",.*,\"Algorithms\":\".*,rsa-min-size=1024,.*,ecc-min-size=192,.*\",.*\}\}"
	test_swtpm_setup_profile "${workdir}" "${profile}" "${exp_response}" "" "" "" "0"
done

echo "Tests with custom profile and --profile-remove-disabled option passed"

# Copy TPM 2 state written by libtpms v0.9 and have the TPM read and re-write the state.
# The size of the state file must not change since we don't want the profile to be
# written into it.

cp "${TESTDIR}/data/tpm2state6/tpm2-00.permall" "${workdir}"
before=$(get_filesize "${workdir}/tpm2-00.permall")

# Avoid swptm sending TPM2_Shutdown(SU_STATE) and adding savestate to the state file
rm -f "${workdir}/logfile"
run_swtpm "${SWTPM_INTERFACE}" \
	--tpm2 \
	--flags not-need-init,startup-clear,disable-auto-shutdown \
	--log "file=${workdir}/logfile"

if ! kill_quiet -0 "${SWTPM_PID}"; then
	echo "Error: ${SWTPM_INTERFACE} TPM did not start."
	exit 1
fi

# Run ChangeEPS so the state is re-written: tsschangeeps
cmd='\x80\x02\x00\x00\x00\x1b\x00\x00\x01\x24\x40\x00\x00\x0c\x00\x00\x00\x09\x40\x00\x00\x09\x00\x00\x00\x00\x00'
RES=$(swtpm_cmd_tx "${SWTPM_INTERFACE}" "${cmd}")
exp=' 80 02 00 00 00 13 00 00 00 00 00 00 00 00 00 00 01 00 00'
if [ "$RES" != "$exp" ]; then
	echo "Error: Did not get expected result from TPM2_ChangeEPS"
	echo "expected: $exp"
	echo "received: $RES"
	exit 1
fi

if ! run_swtpm_ioctl "${SWTPM_INTERFACE}" -s; then
	echo "Error: Could not shut down the ${SWTPM_INTERFACE} TPM."
	exit 1
fi

if wait_process_gone "${SWTPM_PID}" 4; then
	echo "Error: ${SWTPM_INTERFACE} TPM should not be running anymore."
	exit 1
fi

after=$(get_filesize "${workdir}/tpm2-00.permall")
if [ "${after}" -ne "${before}" ]; then
	echo "Error: The size of the TPM 2 state file has changed"
	echo "Actual   : ${after}"
	echo "Expected : ${before}"
	exit 1
fi

echo "Test with state written by libtpms v0.9 passed"

# Check that swtpm emitted log message with OPENSSL_ENABLE_SHA1_SIGNATURES=1 on
# RHEL and CentOS; libtpms 0.9 supported SHA1 signatures
case $(get_distro_name) in
RHEL)	[ "$(get_rhel_version)" -ge 904 ] && exp=1;;
CentOS)	[ "$(get_centos_version)" -ge 900 ] && exp=1;;
*)	exp=0;;
esac
if [ "${exp}" -eq 1 ]; then
	if ! grep -q OPENSSL_ENABLE_SHA1_SIGNATURES "${workdir}/logfile"; then
		echo "Missing reference to OPENSSL_ENABLE_SHA1_SIGNATURES in logfile"
		exit 1
	fi
	echo "Test checking for reference to OPENSSL_ENABLE_SHA1_SIGNATURES in logfile passed"
fi

# If the user passes the null profile in then libtpms has to write state
# at the level of libtpms v0.9 and the size of the state file has to be
# the same as the one created with libtpms v0.9
rm -f "${workdir}"/tpm2-00.*

if ! $SWTPM_SETUP \
	--tpm2 \
	--tpmstate "${workdir}" \
	--config "${workdir}/swtpm_setup.conf" \
	--log "${workdir}/logfile" \
	--tpm "${SWTPM_EXE} socket ${SWTPM_TEST_SECCOMP_OPT}" \
	--profile-file <(echo '{"Name":"null"}')
then
	echo "Test failed: Error: Could not run $SWTPM_SETUP and set null-profile."
	echo "Setup Logfile:"
	cat "${workdir}/logfile"
	exit 1
fi

after=$(get_filesize "${workdir}/tpm2-00.permall")
if [ "${after}" -ne "${before}" ]; then
	echo "Error: The size of the TPM 2 state file is different than if created by libtpms v0.9"
	echo "Actual   : ${after}"
	echo "Expected : ${before}"
	exit 1
fi

echo "Test of writing state at the level of libtpms v0.9 passed"

# Test enablement of PCR banks
rm -f "${workdir}/logfile"
algorithms="rsa,rsa-min-size=1024,hmac,aes,aes-min-size=128,mgf1,keyedhash,xor,sha256,sha384,null,rsassa,rsapss,oaep,ecdsa,kdf1-sp800-56a,kdf2,kdf1-sp800-108,ecc,ecc-nist,symcipher,cmac,ctr,ofb,cbc,cfb,ecb,ecdh"
profile="{\"Name\":\"custom\",\"Algorithms\":\"${algorithms}\"}"
if ! $SWTPM_SETUP \
	--tpm2 \
	--tpmstate "${workdir}" \
	--config "${workdir}/swtpm_setup.conf" \
	--log "${workdir}/logfile" \
	--tpm "${SWTPM_EXE} socket ${SWTPM_TEST_SECCOMP_OPT}" \
	--profile "${profile}" \
	--pcr-banks sha256 \
	--overwrite; then
	echo "Error: swtpm_setup failed to run:"
	cat "${workdir}/logfile"
	exit 1
fi
if ! grep -q "PCR banks sha256 among sha256,sha384." "${workdir}/logfile"; then
	echo "Error: Did not get expected output from activation of SHA256 PCR bank."
	cat "${workdir}/logfile"
	exit 1
fi

# sha512 enablement must fail
rm -f "${workdir}/logfile"
if $SWTPM_SETUP \
	--tpm2 \
	--tpmstate "${workdir}" \
	--config "${workdir}/swtpm_setup.conf" \
	--log "${workdir}/logfile" \
	--tpm "${SWTPM_EXE} socket ${SWTPM_TEST_SECCOMP_OPT}" \
	--profile "${profile}" \
	--pcr-banks sha512 \
	--overwrite; then
	echo "Error: Enablement of SHA512 bank should have failed."
	cat "${workdir}/logfile"
	exit 1
fi
echo "Test of activation of sha256 bank when only sha256 & sha384 banks are available passed"

exit 0
